package cinvestment;

import ilog.concert.IloLinearNumExpr;
import ilog.concert.IloNumVar;
import ilog.cplex.IloCplex;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

import support.AlgorithmSupport;
import util.Lang;
import util.MathUtil;
import util.NullOutputStream;

/**
 * CapacityInvestment is programmed to solve the stochastic programming problem
 * based on Bassamboo. And the class implement the classic L-Shaped algorithm
 * based on single-cut. The method in this class considers fixed cost of opening
 * a facility.
 * **/
public class CapacityInvestmentSingleCut {

	public CapacityInvestmentSingleCut(double delta, int productTypes,
			int simulationCount, double[] penalties, double c1, double ratio) {
		this.delta = delta;
		this.N = productTypes;
		this.simulatedCount = simulationCount;
		this.p = Lang.clone(penalties);
		this.c1 = c1;
		this.ratio = ratio;
	}

	private List<Plant> plants;
	private double[][] demands;

	/**
	 * Number of kinds of products
	 */
	private int N;

	/**
	 * Penalty cost for unfilled demand
	 * **/
	// private double p = 1.0;
	// try to vary the penalty cost of different products
	private double[] p;

	/**
	 * Number of plants
	 */
	private int n;

	/**
	 * Tolerance for the gap between Upper bound and lower bound
	 * **/
	private double epsilon = 0.001;

	/**
	 * Basic cost for flexible resource cost
	 * **/
	private double c1;
	private double delta;

	/**
	 * Sampling count of demand
	 * **/
	private int simulatedCount;

	/**
	 * iteration counter
	 * **/
	private int iter = 1;

	private double probability;

	/**
	 * Store the multiplier of dual problem for each scenario in each iteration
	 * **/
	private List<Double[][]> pi;
	private List<Double[][]> sigma;

	private void init() {
		// this.demands = AlgorithmSupport.getDemands_uniform(simulatedCount, N,
		// 0.0, 2.0);
		this.demands = AlgorithmSupport.getDemands_normal(simulatedCount, N,
				1.0, 0.3, 0.0, 2.0);
		this.createPlants();
		this.n = this.plants.size();

		this.pi = new ArrayList<>();
		this.sigma = new ArrayList<>();

		this.probability = BigDecimal
				.valueOf(1.0)
				.divide(BigDecimal.valueOf(simulatedCount), 5,
						BigDecimal.ROUND_HALF_EVEN).doubleValue();
	}

	public List<Double[]> lshapedCalculate() {
		this.init();

		// // store the iteration result to file
		// List<Double[]> outputData = new ArrayList<>();

		List<Double[]> finalResult = new ArrayList<>();

		iter = 1;
		while (true) {
			System.out.println("@ iteration " + iter);

			/** step 1 **/
			MasterResult mr = null;
			// TODO
			// to think about what is the initial condition
			if (iter == 1) {
				double[] capacity = new double[n];
				int[] x = new int[n];
				for (int j = 0; j < n; j++)
					capacity[j] = 1.0;
				mr = new MasterResult();
				mr.setValues(0.0, capacity, x, Double.NEGATIVE_INFINITY);
			} else
				mr = this.calculateMasterProblem(probability);

			/** step 2 **/
			// double yObj = 0.0;

			for (int m = 0; m < simulatedCount; m++) {
				this.calculateDualProblem(mr.capacities, m);
			}

			double w = 0.0;
			for (int m = 0; m < simulatedCount; m++) {
				for (int i = 0; i < N; i++) {
					w += this.demands[m][i] * this.pi.get(iter - 1)[m][i];
				}
				for (int j = 0; j < n; j++)
					w += this.sigma.get(iter - 1)[m][j] * mr.capacities[j];
			}
			w = w * this.probability;

			if (w - mr.theta < this.epsilon || iter > 350) {
				if (w - mr.theta < this.epsilon)
					System.out.println("==Optimal results iter=" + iter
							+ "======");
				else
					System.out.println("==Force to stop iter=" + iter
							+ "======");
				System.out.println("Gap = "
						+ MathUtil.DOUBLE_FORMAT_3.format(mr.theta - w));

				for (int j = 0; j < n; j++)
					System.out.println("plant " + j + ": \t"
							+ MathUtil.DOUBLE_FORMAT_3.format(mr.capacities[j])
							+ "\t" + this.plants.get(j).getAvailableProducts());

				Double[] fobj = { mr.objValue };
				Double[] fcap = new Double[n];
				fcap = Lang.convertdouble2Double(mr.capacities);
				finalResult.add(fobj);
				finalResult.add(fcap);

				break;
			}
			System.out.println("Gap = "
					+ MathUtil.DOUBLE_FORMAT_3.format(mr.theta - w));

			for (int j = 0; j < n; j++)
				System.out.println("plant " + j + ": \t" + mr.openFlag[j]
						+ "\t"
						+ MathUtil.DOUBLE_FORMAT_3.format(mr.capacities[j])
						+ "\t" + this.plants.get(j).getAvailableProducts());
			iter++;
		}
		return finalResult;
	}

	/**
	 * Solve the master problem
	 * **/
	private MasterResult calculateMasterProblem(double probability) {
		try {
			IloCplex cplex = new IloCplex();
			cplex.setOut(new NullOutputStream());
			IloNumVar[] K = cplex.numVarArray(n, 0.0, Double.MAX_VALUE);
			IloNumVar[] X = cplex.boolVarArray(n);

			IloLinearNumExpr obj = cplex.linearNumExpr();
			for (int j = 0; j < n; j++) {
				double fixedCost = this.getFixedCost(plants.get(j)
						.getAvailableProductsNumber());
				double flexibleCost = this.getFlexibleCost(plants.get(j)
						.getAvailableProductsNumber());
				obj.addTerm(fixedCost, X[j]);
				obj.addTerm(flexibleCost, K[j]);
			}

			IloNumVar theta = cplex.numVar(Double.NEGATIVE_INFINITY,
					Double.MAX_VALUE);

			// if there are generated cuts
			if (this.pi != null && this.pi.size() > 0) {
				obj.addTerm(1.0, theta);

				for (int iter = 0; iter < pi.size(); iter++) {
					IloLinearNumExpr r1 = cplex.linearNumExpr();
					double r2 = 0.0;
					for (int m = 0; m < simulatedCount; m++) {
						for (int j = 0; j < n; j++)
							r1.addTerm(probability * sigma.get(iter)[m][j],
									K[j]);
						for (int i = 0; i < N; i++) {
							r2 += probability * pi.get(iter)[m][i]
									* this.demands[m][i];
						}
					}
					cplex.addGe(theta, cplex.sum(r1, r2));
				}
			}

			cplex.addMinimize(obj);

			for (int j = 0; j < n; j++) {
				cplex.addGe(cplex.prod(999.0, X[j]), K[j]);
			}

			// cplex.exportModel(Lang.projectRoot() + "\\data\\master\\master"
			// + iter + ".lp");

			MasterResult mr = new MasterResult();
			if (cplex.solve()) {
				int[] Xv = new int[n];
				for (int j = 0; j < n; j++)
					Xv[j] = (int) cplex.getValue(X[j]);
				double thetaValue = Double.NEGATIVE_INFINITY;

				if (this.pi != null && this.pi.size() > 0) {
					thetaValue = cplex.getValue(theta);
				}
				mr.setValues(cplex.getObjValue(), cplex.getValues(K), Xv,
						thetaValue);
			}
			cplex.end();
			return mr;

		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * Solve the subProblem
	 * 
	 * @return optimal value
	 * **/
	private double calculateSubProblem(double[] Kvalue, int m) {
		double result = 0.0;
		try {
			IloCplex cplex = new IloCplex();
			cplex.setOut(new NullOutputStream());
			IloLinearNumExpr obj = cplex.linearNumExpr();
			IloNumVar[] x = cplex.numVarArray(n * N, 0.0, Double.MAX_VALUE);
			for (int i = 0; i < N; i++) {
				for (int j = 0; j < n; j++) {
					if (plants.get(j).isAvailable(i))
						obj.addTerm(-p[i], x[i * n + j]);
				}
			}
			cplex.addMinimize(obj);

			for (int i = 0; i < N; i++) {
				IloLinearNumExpr left = cplex.linearNumExpr();
				for (int j = 0; j < n; j++) {
					if (plants.get(j).isAvailable(i))
						left.addTerm(1.0, x[i * n + j]);
				}
				cplex.addLe(left, this.demands[m][i]);
			}
			for (int j = 0; j < n; j++) {
				IloLinearNumExpr left = cplex.linearNumExpr();
				for (int i = 0; i < N; i++) {
					if (plants.get(j).isAvailable(i))
						left.addTerm(1.0, x[i * n + j]);
				}
				cplex.addLe(left, Kvalue[j]);
			}

			// cplex.exportModel(Lang.projectRoot()
			// + "\\data\\subproblem\\subproblem" + iter + "_" + m + ".lp");

			if (cplex.solve()) {
				result = cplex.getObjValue();
			}
			cplex.end();

		} catch (Exception e) {
			e.printStackTrace();
		}
		return result;
	}

	/**
	 * Solve the dual problem
	 * 
	 * @param m
	 *            :scenario
	 * @return a double vector (pi_1,pi2)
	 * **/
	private void calculateDualProblem(double[] Kvalue, int m) {
		try {
			IloCplex cplex = new IloCplex();
			// turn off the log output to console
			cplex.setOut(new NullOutputStream());
			IloNumVar[] pi_m = cplex.numVarArray(N, Double.NEGATIVE_INFINITY,
					0.0);
			IloNumVar[] sigma_m = cplex.numVarArray(n,
					Double.NEGATIVE_INFINITY, 0.0);

			IloLinearNumExpr obj = cplex.linearNumExpr();
			for (int i = 0; i < N; i++) {
				obj.addTerm(this.demands[m][i], pi_m[i]);
			}
			for (int j = 0; j < n; j++)
				obj.addTerm(Kvalue[j], sigma_m[j]);
			cplex.addMaximize(obj);

			for (int i = 0; i < N; i++) {
				for (int j = 0; j < n; j++) {
					if (this.isArcInConfiguration(i, j))
						cplex.addLe(cplex.sum(pi_m[i], sigma_m[j]), -p[i]);
				}
			}

			// cplex.exportModel(Lang.projectRoot() + "\\data\\dual\\dual" +
			// iter
			// + "_" + m + ".lp");

			if (cplex.solve()) {
				if (this.pi.size() < iter) {
					this.pi.add(new Double[simulatedCount][N]);
					this.sigma.add(new Double[simulatedCount][n]);
				}
				for (int i = 0; i < N; i++)
					this.pi.get(iter - 1)[m][i] = cplex.getValue(pi_m[i]);
				for (int j = 0; j < n; j++)
					this.sigma.get(iter - 1)[m][j] = cplex.getValue(sigma_m[j]);
			}
			cplex.end();

		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private class MasterResult {
		double theta;
		double objValue;

		double[] capacities;
		int[] openFlag;

		void setValues(double objValue, double[] vars, int[] open, double theta) {
			this.objValue = objValue;
			this.capacities = Lang.clone(vars);
			this.openFlag = Lang.clone(open);
			this.theta = theta;
		}
	}

	private double ratio;

	/**
	 * get fixed cost
	 * 
	 * @param number
	 *            : fixed cost varies based on the number of products a plant
	 *            can produce
	 * */
	private double getFixedCost(int number) {
		// try the linear relationship between fixed cost and number
		// return 0.5 * number;
		return BigDecimal.valueOf(this.ratio).multiply(BigDecimal.valueOf(c1))
				.setScale(5, BigDecimal.ROUND_HALF_EVEN).doubleValue();
		// return 0.5;
	}

	/**
	 * create a list of plants
	 * **/
	private void createPlants() {
		this.plants = new ArrayList<>();
		int[] set = MathUtil.getSetFromZ(N);
		List<List<Integer>> subSetList = MathUtil.subSetOrder(set);
		for (List<Integer> subList : subSetList) {
			Plant plant = new Plant();
			plant.setAvailableProducts(subList);
			plants.add(plant);
		}
	}

	/**
	 * Calculate the flexible cost for a plant based on the number of products
	 * it can produce. c1[1+(countj-1)*delta]
	 * 
	 * @param number
	 *            : the number of products the plant can produce
	 * **/
	private double getFlexibleCost(int number) {
		return BigDecimal
				.valueOf(c1)
				.multiply(
						BigDecimal.valueOf(1).add(
								BigDecimal.valueOf(delta).multiply(
										BigDecimal.valueOf(number - 1))))
				.doubleValue();
	}

	private boolean isArcInConfiguration(int i, int j) {
		// // without considering some special configuration
		// //
		// if (!this.longChain)
		return plants.get(j).isAvailable(i);

		// // long chain configuration
		// List<Integer> subSet = plants.get(j).getAvailableProducts();
		// if (i != N - 1) {
		// return subSet.size() == 2 && subSet.contains(i)
		// && subSet.contains(i + 1);
		// } else {
		// return subSet.size() == 2 && subSet.contains(i)
		// && subSet.contains(0);
		// }
	}
}
